use std::collections::HashMap;
use anyhow::{anyhow, Result};
use dotenv;

use clap::App;

use hyper::Client;

use std::fs;
use std::io::Read;
use std::path::PathBuf;
use hyper::client::HttpConnector;
use crate::miner::SectorStatus;

mod miner;

#[derive(Clone, Debug)]
struct SectorId {
    pub number: u64,
    pub miner: u64,
}

impl SectorId {
    pub fn parse(name: &str) -> Result<SectorId> {
        let vec: Vec<&str> = name.split("-").collect();
        if vec.len() != 3 {
            return Err(anyhow::anyhow!("wrong format of sector id {}", name));
        }
        let miner = vec[1].trim_start_matches("t0");
        let miner: u64 = miner.parse()?;
        let number: u64 = vec[2].parse()?;
        let id = SectorId {
            miner,
            number,
        };
        Ok(id)
    }

    pub fn to_name(&self) -> String {
        format!("s-t0{}-{}", self.miner, self.number)
    }
}

#[derive(Clone, Debug)]
struct Sector {
    pub id: SectorId,
    pub unsealed: bool,
    pub sealed: bool,
    pub cache: bool,
}

#[tokio::main]
async fn main() -> Result<()> {
    dotenv::dotenv().ok();

    let matches = App::new("cleaner")
        .version("0.1.0")
        .author("loop")
        .about("sector clean tools")
        .arg("-r, --remove 'do remove sector file.'")
        .get_matches();

    let (token, url) = parse_miner()?;
    let client = Client::new();

    let worker_path = dotenv::var("WORKER_PATH");
    if worker_path.is_err() {
        println!("'WORKER_PATH' not set!");
        return Ok(())
    }
    let worker_path = worker_path.unwrap();
    println!("worker home {}", worker_path);

    let home = PathBuf::from(worker_path);

    let unsealed = home.join("unsealed");
    let sealed = home.join("sealed");
    let cache = home.join("cache");
    let mut map = HashMap::<u64, Sector>::new();

    let unsealed_sectors = scan_path(unsealed)?;
    let sealed_sectors = scan_path(sealed)?;
    let cache_sectors = scan_path(cache)?;

    for v in unsealed_sectors {
        let sc = map.get_mut(&v.number);
        match sc {
            Some(sc) => sc.unsealed = true,
            None => {
                let sc = Sector {
                    id: v.clone(),
                    unsealed: true,
                    sealed: false,
                    cache: false,
                };
                map.insert(v.number, sc);
            }
        }
    }

    for v in sealed_sectors {
        let sc = map.get_mut(&v.number);
        match sc {
            Some(sc) => sc.sealed = true,
            None => {
                let sc = Sector {
                    id: v.clone(),
                    unsealed: false,
                    sealed: true,
                    cache: false,
                };
                map.insert(v.number, sc);
            }
        }
    }

    for v in cache_sectors {
        let sc = map.get_mut(&v.number);
        match sc {
            Some(sc) => sc.cache = true,
            None => {
                let sc = Sector {
                    id: v.clone(),
                    unsealed: false,
                    sealed: false,
                    cache: true,
                };
                map.insert(v.number, sc);
            }
        }
    }

    let remove = matches.is_present("remove");
    let mut removing = Vec::<Sector>::new();

    for (_, sector) in map {
        let rsp = miner::query_sector_status(&client, &url, &token, sector.id.number).await?;
        if rsp.error.is_none() {
            let status = rsp.result.unwrap();
            println!("{:?}", status);
            if status.state.eq("Proving") || status.state.eq("Removing") || status.state.eq("Removed") {
                removing.push(sector.clone());
            }
        } else {
            let err = rsp.error.unwrap();
            println!("query sector status error, {}, {:?}", sector.id.number, err)
        }
    }
    println!("can clean sector count {}", removing.len());
    let mut vec = Vec::<u64>::new();
    for sector in removing {
        vec.push(sector.id.number);
        if sector.unsealed {
            let path = home.join("unsealed").join(sector.id.to_name());
            if remove {
                println!("remove {}", path.display());
                let _ = fs::remove_file(path);
            } else {
                println!("{}", path.display());
            }
        }
        if sector.sealed {
            let path = home.join("sealed").join(sector.id.to_name());
            if remove {
                println!("remove {}", path.display());
                let _ = fs::remove_file(path);
            } else {
                println!("{}", path.display());
            }
        }
        if sector.cache {
            let path = home.join("cache").join(sector.id.to_name());
            if remove {
                println!("remove {}", path.display());
                let _ = fs::remove_file(path);
            } else {
                println!("{}", path.display());
            }
        }
    }
    println!("remove sectors: ");
    println!("{:?}", vec);
    Ok(())
}

fn parse_miner() -> Result<(String, String)> {
    let mut store_home = dotenv::var("LOTUS_MINER_PATH");
    if store_home.is_err() {
        store_home = dotenv::var("LOTUS_STORAGE_PATH");
    }
    if let Err(err) = store_home {
        println!(
            "both env 'LOTUS_MINER_PATH' and 'LOTUS_MINER_PATH' not found. can't get miner repo."
        );
        return Err(anyhow::Error::from(err));
    }
    let store_home = store_home.unwrap();
    println!("store home {}", store_home);
    let store_home = PathBuf::from(store_home);
    let api_path = store_home.join("api");
    let api = read_file_to_string(api_path)?;
    let token_path = store_home.join("token");
    let token = read_file_to_string(token_path)?;

    let api_info: Vec<&str> = api.split("/").collect();
    if api_info.len() != 6 {
        return Err(anyhow::Error::msg("api format not right."));
    };

    let api_ip = api_info[2];
    let api_port = api_info[4];
    let miner_url = format!("http://{}:{}/rpc/v0", api_ip, api_port);

    println!("token: {}", token);
    println!("miner: {}", miner_url);
    Ok((token, miner_url))
}

fn read_file_to_string(path: PathBuf) -> Result<String> {
    let mut file = fs::File::open(path)?;
    let mut buf = String::new();
    let _ = file.read_to_string(&mut buf)?;
    Ok(buf)
}

fn scan_path(path: PathBuf) -> Result<Vec<SectorId>> {
    let mut vec = Vec::<SectorId>::new();
    if path.is_dir() {
        for entry in fs::read_dir(path.clone())? {
            let entry = entry?;
            let name = entry.file_name();
            let name = name.to_str().ok_or(anyhow!("read dir error {:?}", path))?;
            println!("scan: {:?}", entry.path());
            let ret = SectorId::parse(&name);
            match ret {
                Err(e) => println!("parse sector error, {}, {:?}", &name, e),
                Ok(sector) => vec.push(sector),
            }
        }
    }
    let mut miner = 0u64;
    for v in vec.iter() {
        if miner == 0 {
            miner = v.miner
        } else {
            if miner != v.miner {
                return Err(anyhow!("exists different miner, path {:?}, {}, {}", path, miner, v.miner))
            }
        }
    }
    Ok(vec)
}
